/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.validator;

import java.lang.annotation.*;
import java.lang.ref.*;
import java.util.*;
import java.util.concurrent.*;
import ch.trackedbean.common.*;
import ch.trackedbean.util.*;
import ch.trackedbean.validator.defaultValidators.*;
import ch.trackedbean.validator.engine.*;

/**
 * Manager holding {@link IValidator}s.<br>
 * Create <code>META-INF/services/ch.trackedbean.Validator.properties</code> to configure the manager directly.<br>
 * Here an example of such a file:<br>
 * All shown block were optional so you may define just the ones you need.
 * 
 * <pre>
 * ###Factory to use -&gt; must be of type IValidatorFactory###
 * factory=ch.gugus.Factory
 * 
 * ###Messageinterpolator to use -&gt; must be of type IInterpolator###
 * interpolator=ch.gugus.Interpolator
 * 
 * ###Mapping from 'external' annotations to validators###
 * #List defining the following properties
 * annotationMappings=an1,an2
 * #Entries to map the annotation EMail to the MailValidator
 * an1.annotation=ch.myAnnotations.EMail
 * an1.validator=ch.trackedbean.validator.defaultValidators.MailValidator
 * #Entries to map the annotation Regex to the RegexValidator
 * an2.annotation=ch.myAnnotations.Regex
 * an2.validator=ch.trackedbean.validator.defaultValidators.RegexValidator
 * </pre>
 * 
 * @author M. Hautle
 */
// TODO parameter mapping
@SuppressWarnings("unchecked")
public class ValidatorManager {
	/** Empty validator. */
	private static final IValidator NOP_VALIDATOR = new Validator(Object.class);

	/** Mapping for "external" annotations on defined validators. */
	private static final Map<Class<? extends Annotation>, ExternalAnnotationMapping> VALIDATOR_MAPPING = new ConcurrentHashMap<Class<? extends Annotation>, ExternalAnnotationMapping>();

	/** The factory to use. */
	private static IValidatorFactory factory = new DefaultFactory();

	/** The message interpolator used by the {@link AbstractValidator}s. */
	private static IInterpolator messageInterpolator = new DefaultInterpolator();

	/** Validator cache. */
	private static final Map<Class<?>, Reference<IValidator>> VALIDATOR_CACHE = new ConcurrentHashMap<Class<?>, Reference<IValidator>>();

	static {
		final Properties p = ConfigurationHelper.loadSettings("ch.trackedbean.Validator");
		if (p != null) {
			// factory
			final Object f = ConfigurationHelper.create(p, "factory");
			if (f instanceof IValidatorFactory)
				setFactory((IValidatorFactory) f);
			// interpolator
			final Object i = ConfigurationHelper.create(p, "interpolator");
			if (i instanceof IInterpolator)
				setMessageInterpolator((IInterpolator) i);
			// extetrnal annotations
			for (String m : ConfigurationHelper.getListEntries(p, "annotationMappings")) {
				final Class<Object> an = ConfigurationHelper.getClass(p, m + ".annotation");
				final Class<Object> val = ConfigurationHelper.getClass(p, m + ".validator");
				if (an != null && val != null)
					addAnnotationMapping(new ExternalAnnotationMapping(an, val));
			}
		}
		VALIDATOR_CACHE.put(Object.class, new SoftReference<IValidator>(NOP_VALIDATOR));
	}

	/**
	 * Returns the validator for the specified class
	 * 
	 * @param c The validator target class
	 * @return The corresponding validator
	 * @throws ValidatorInitialisationException If something went wrong
	 */
	public static IValidator getValidator(Class<?> c) throws ValidatorInitialisationException {
		final Reference<IValidator> ref = VALIDATOR_CACHE.get(c);
		IValidator val = ref != null ? ref.get() : null;
		if (val == null) {
			val = factory.createValidator(c);
			if (!val.doesSomething())
				val = NOP_VALIDATOR;
			VALIDATOR_CACHE.put(c, new SoftReference(val));
		}
		return val;
	}

	/**
	 * Validates the given bean.<br>
	 * Consider using {@link #ensureCorrectness(Object)} for serverside validations.
	 * 
	 * @param bean The bean to validate
	 * @return The validation result or null if null was passed
	 * @throws TechnicalException If something went wrong
	 */
	public static ValidationResult validate(Object bean) throws TechnicalException {
		if (bean == null)
			return null;
		return getValidator(bean.getClass()).validate(bean);
	}

	/**
	 * Ensures that the given bean is valide.<br>
	 * If the passed bean contains errors a {@link ValidationException} wrapping the {@link ValidationResult} is thrown.<br>
	 * Consider using {@link #validate(Object)} for clientside validations.
	 * 
	 * @param bean The bean
	 * @throws ValidationException If the bean is not correct
	 * @throws TechnicalException If the bean is null or something went wrong
	 */
	public static void ensureCorrectness(Object bean) throws ValidationException {
		if (bean == null)
			throw new IllegalArgumentException("The passed bean was null!");
		final ValidationResult res = getValidator(bean.getClass()).validate(bean);
		if (!res.isBeanValid())
			throw new ValidationException(res);
	}

	/**
	 * Validates the business part of the given bean.
	 * 
	 * @param bean The bean to validate
	 * @return The validation result or null if null was passed
	 * @throws TechnicalException If something went wrong
	 */
	public static ValidationResult validateBusinessPart(Object bean) throws TechnicalException {
		if (bean == null)
			return null;
		return getValidator(bean.getClass()).validateBusinessPart(bean);
	}

	/**
	 * Validates a property of the given bean.<br>
	 * If one of the passed parameters is null the method will also return null.
	 * 
	 * @param property The property name
	 * @param bean The bean
	 * @return null if the property was valid, otherwise the erros descriptions
	 * @throws TechnicalException If the property doesen't exists or something went wrong
	 */
	public static IErrorDescription[] validate(String property, Object bean) throws TechnicalException {
		if (bean == null || property == null || property.length() == 0)
			return null;
		return getValidator(bean.getClass()).validate(property, bean);
	}

	/**
	 * Registers the given external mapping.<br>
	 * Use this method for "external" annotations (like HibernateValidator- or JPA-Annotations).
	 * 
	 * @param mapping The mapping
	 */
	public static void addAnnotationMapping(ExternalAnnotationMapping mapping) {
		VALIDATOR_MAPPING.put(mapping.getExternalAnnotationType(), mapping);
	}

	/**
	 * Returns the registred validator mapping for the given annotation.
	 * 
	 * @param <T> The annotation type
	 * @param <V> The validator type
	 * 
	 * @param annotation The annotation to lookup
	 * @return The mapped validator mapping or null if no mapping exists
	 */
	public static <T extends Annotation, V extends IPropertyValidator> ExternalAnnotationMapping<T, V> getMappedValidator(Class<T> annotation) {
		return VALIDATOR_MAPPING.get(annotation);
	}

	/**
	 * Returns the default message interpolator.<br>
	 * You may use this interpolator for the implementation of {@link IErrorDescription#getStringRepresentation()}.
	 * 
	 * @return The message interpolator
	 */
	public static IInterpolator getMessageInterpolator() {
		return messageInterpolator;
	}

	/**
	 * Sets the default message interpolator.
	 * 
	 * @param interpolator The interpolator to use
	 */
	public static void setMessageInterpolator(IInterpolator interpolator) {
		if (interpolator == null)
			throw new IllegalArgumentException("You must set a non null value as interpolator!");
		messageInterpolator = interpolator;
	}

	/**
	 * Sets the factory used to create the {@link IValidator}s.
	 * 
	 * @param factory The factory to use
	 */
	public static void setFactory(IValidatorFactory factory) {
		if (factory == null)
			throw new IllegalArgumentException("You must set a non null value as factory!");
		ValidatorManager.factory = factory;
	}

	/**
	 * Returns a string representation of the passed errors.<br>
	 * This method simply concatenates the results of {@link IErrorDescription#getStringRepresentation()}.
	 * 
	 * @param errors The errors or null
	 * @return The error string or null if null was passed
	 */
	public static String buildErrorString(IErrorDescription[] errors) {
		if (errors == null || errors.length == 0)
			return null;
		final StringBuilder str = new StringBuilder();
		for (IErrorDescription e : errors)
			str.append(e.getStringRepresentation()).append('\n');
		str.setLength(str.length() - 1);
		return str.toString();
	}

	/**
	 * Default implementation of {@link IValidatorFactory}.
	 * 
	 * @author M. Hautle
	 */
	public static class DefaultFactory implements IValidatorFactory {
		/**
		 * {@inheritDoc}
		 */
		@Override
		public IValidator createValidator(Class<?> type) throws ValidatorInitialisationException {
			return new DefaultValidatorConfigurator(type).create();
		}
	}
}
